package com.google.code.sms.service;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;

import com.google.code.sms.domain.MediaElement;
import com.google.code.sms.domain.MediaFolder;
import com.google.code.sms.domain.MediaElementIndex;
import com.google.code.sms.domain.MediaElementIndex.SortableMediaElement;

/**
 * Provides services for grouping media elements by index.
 *
 * @author Sindre Mehus
 */
public class MediaElementIndexService {

    private SettingsService settingsService;
    private MediaElementService mediaElementService;

    /**
     * Returns a map from media element indexes to sets of media elements that are direct
     * children of the given media folders.
     *
     *
     * @param folders The media folders.
     * timestamp of the media folders.
     * @return A map from media element indexes to sets of media elements that are direct
     * children of this media element.
     * @throws IOException If an I/O error occurs.
     */
    public SortedMap<MediaElementIndex, SortedSet<MediaElementIndex.SortableMediaElementWithMediaElements>> getIndexedMediaElementsFromMediaFolders(List<MediaFolder> folders) throws IOException {
        SortedSet<MediaElementIndex.SortableMediaElementWithMediaElements> mediaElements = createSortableMediaElementsFromMediaFolders(folders);
        return sortMediaElements(mediaElements);
    }

    public SortedMap<MediaElementIndex, SortedSet<MediaElementIndex.SortableMediaElementWithMediaElement>> getIndexedMediaElements(List<MediaElement> mediaElements) throws IOException {
        SortedSet<MediaElementIndex.SortableMediaElementWithMediaElement> sortableMediaElements = createSortableMediaElements(mediaElements);
        return sortMediaElements(sortableMediaElements);
    }

    private <T extends SortableMediaElement> SortedMap<MediaElementIndex, SortedSet<T>> sortMediaElements(SortedSet<T> elements) {
        List<MediaElementIndex> indexes = createIndexesFromExpression(settingsService.getIndexString());
        Comparator<MediaElementIndex> indexComparator = new MusicIndexComparator(indexes);

        SortedMap<MediaElementIndex, SortedSet<T>> result = new TreeMap<>(indexComparator);

        for (T element : elements) {
            MediaElementIndex index = getIndex(element, indexes);
            SortedSet<T> elementSet = result.get(index);
            if (elementSet == null) {
                elementSet = new TreeSet<>();
                result.put(index, elementSet);
            }
            elementSet.add(element);
        }

        return result;
    }

    /**
     * Creates a new instance by parsing the given expression. The expression
     * consists of an index name, followed by an optional list of one-character
     * prefixes. For example:
     * <p/>
     * <
     * p/>
     * The expression <em>"A"</em> will create the index <em>"A" -&gt;
     * ["A"]</em><br/>
     * The expression <em>"The"</em> will create the index <em>"The" -&gt;
     * ["The"]</em><br/>
     * The expression <em>"A(A&Aring;&AElig;)"</em> will create the index
     * <em>"A" -&gt; ["A", "&Aring;", "&AElig;"]</em><br/>
     * The expression <em>"X-Z(XYZ)"</em> will create the index <em>"X-Z" -&gt;
     * ["X", "Y", "Z"]</em>
     *
     * @param expr The expression to parse.
     * @return A new instance.
     */
    protected MediaElementIndex createIndexFromExpression(String expr) {
        int separatorIndex = expr.indexOf('(');
        if (separatorIndex == -1) {

            MediaElementIndex index = new MediaElementIndex(expr);
            index.addPrefix(expr);
            return index;
        }

        MediaElementIndex index = new MediaElementIndex(expr.substring(0, separatorIndex));
        String prefixString = expr.substring(separatorIndex + 1, expr.length() - 1);
        for (int i = 0; i < prefixString.length(); i++) {
            index.addPrefix(prefixString.substring(i, i + 1));
        }
        return index;
    }

    /**
     * Creates a list of music indexes by parsing the given expression. The
     * expression is a space-separated list of sub-expressions, for which the
     * rules described in {@link #createIndexFromExpression} apply.
     *
     * @param expr The expression to parse.
     * @return A list of music indexes.
     */
    protected List<MediaElementIndex> createIndexesFromExpression(String expr) {
        List<MediaElementIndex> result = new ArrayList<>();

        StringTokenizer tokenizer = new StringTokenizer(expr, " ");
        while (tokenizer.hasMoreTokens()) {
            MediaElementIndex index = createIndexFromExpression(tokenizer.nextToken());
            result.add(index);
        }

        return result;
    }

    private SortedSet<MediaElementIndex.SortableMediaElementWithMediaElements> createSortableMediaElementsFromMediaFolders(List<MediaFolder> folders) throws IOException {
        String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray();
        SortedMap<String, MediaElementIndex.SortableMediaElementWithMediaElements> mediaElementMap = new TreeMap<>();

        for (MediaFolder folder : folders) 
        {
            List<MediaElement> children = mediaElementService.getChildrenOf(folder.getPath(), true, true, true, true);
            
            for (MediaElement child : children)
            {
                String sortableName = createSortableName(child.getName(), ignoredArticles);
                MediaElementIndex.SortableMediaElementWithMediaElements mediaElementIndex = mediaElementMap.get(sortableName);
                if (mediaElementIndex == null) {
                    mediaElementIndex = new MediaElementIndex.SortableMediaElementWithMediaElements(child.getName(), sortableName);
                    mediaElementMap.put(sortableName, mediaElementIndex);
                }
                mediaElementIndex.addMediaElement(child);
            }
        }

        return new TreeSet<>(mediaElementMap.values());
    }

    private SortedSet<MediaElementIndex.SortableMediaElementWithMediaElement> createSortableMediaElements(List<MediaElement> mediaElements) {
        TreeSet<MediaElementIndex.SortableMediaElementWithMediaElement> result = new TreeSet<>();
        String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray();
        for (MediaElement mediaElement : mediaElements) {
            String sortableName = createSortableName(mediaElement.getName(), ignoredArticles);
            result.add(new MediaElementIndex.SortableMediaElementWithMediaElement(mediaElement.getName(), sortableName, mediaElement));
        }

        return result;
    }

    private String createSortableName(String name, String[] ignoredArticles) {
        String uppercaseName = name.toUpperCase();
        for (String article : ignoredArticles) {
            if (uppercaseName.startsWith(article.toUpperCase() + " ")) {
                return name.substring(article.length() + 1) + ", " + article;
            }
        }
        return name;
    }

    /**
     * Returns the media element index to which the given media element belongs.
     *
     * @param mediaElement The Media Element in question.
     * @param indexes List of available indexes.
     * @return The music index to which this music file belongs, or
     * {@link MediaElementIndex#OTHER} if no index applies.
     */
    private MediaElementIndex getIndex(SortableMediaElement mediaElement, List<MediaElementIndex> indexes) {
        String sortableName = mediaElement.getSortableName().toUpperCase();
        for (MediaElementIndex index : indexes) {
            for (String prefix : index.getPrefixes()) {
                if (sortableName.startsWith(prefix.toUpperCase())) {
                    return index;
                }
            }
        }
        return MediaElementIndex.OTHER;
    }

    public void setSettingsService(SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    public void setMediaElementService(MediaElementService mediaElementService) {
        this.mediaElementService = mediaElementService;
    }

    private static class MusicIndexComparator implements Comparator<MediaElementIndex>, Serializable {

        private List<MediaElementIndex> indexes;

        public MusicIndexComparator(List<MediaElementIndex> indexes) {
            this.indexes = indexes;
        }

        @Override
        public int compare(MediaElementIndex a, MediaElementIndex b) {
            int indexA = indexes.indexOf(a);
            int indexB = indexes.indexOf(b);

            if (indexA == -1) {
                indexA = Integer.MAX_VALUE;
            }
            if (indexB == -1) {
                indexB = Integer.MAX_VALUE;
            }

            if (indexA < indexB) {
                return -1;
            }
            if (indexA > indexB) {
                return 1;
            }
            return 0;
        }
    }
}